package com.theeviljames.coursework.ga;

import java.util.Arrays;
import java.util.Hashtable;
import java.util.Random;

import com.theeviljames.coursework.ann.ANN;
import com.theeviljames.coursework.ann.ANNMatrix;
import com.theeviljames.coursework.problems.IANNProblem;

public class ANNGa {

	private int popSize;
	private int maxEpochs;
	private int[] topology;
	private ANN ann;
	private double mutateRate;
	private Genome[] population;
	private static final Random rand  = new Random();
	private Hashtable<Integer,Double> bestError = new Hashtable<Integer, Double>();
	private Hashtable<Integer,Double> sumError = new Hashtable<Integer, Double>();
	private int noOfElite;
	public int mutateCounter = 0;
	private IANNProblem problem;
	
	public ANNGa(int popSize, int maxEpochs, int[] topology, double mutateRate, int noOfElite, IANNProblem problem) {
		this.popSize = popSize;
		this.maxEpochs = maxEpochs;
		this.topology = topology;
		this.mutateRate = mutateRate;
		this.noOfElite = noOfElite;
		this.problem = problem;
		population = new Genome[popSize];
		ann = new ANN(topology, 0.0, 0.0, true);
		for(int i = 0; i < popSize; i++){
			double[][][] weights = new double[topology.length-1][][];
			for(int j = 0; j < topology.length-1; j++){
				weights[j] = ANNMatrix.random(topology[j]+1, topology[j+1], 1, rand);
			}
			Genome g = new Genome(weights);
			g.setFitness(fitness(g.getWeights()));
			population[i] = g;
		}
		Arrays.sort(population);
	}
	
	
	public Genome run(){
		bestError.put(0, bestErrorValue());
		sumError.put(0, sumErrorVal());
		for(int i = 1; i <= maxEpochs; i++){
			Genome[] newPopulation = new Genome[popSize];
			for(int j = 0; j < ((popSize-noOfElite)/2); j++){
				Genome p1 = selectByRank();
				Genome p2 = selectByRank();
				Genome[] children = crossover2(p1, p2);
				newPopulation[2*j] = children[0];
				newPopulation[(2*j)+1] = children[1];
			}
			for(int j = 0; j < noOfElite; j++)newPopulation[popSize-noOfElite+j] = population[j];
			population = newPopulation;
			Arrays.sort(population);
			bestError.put(i, bestErrorValue());
			sumError.put(i, sumErrorVal());
			if(bestErrorValue()<0.01)break;
		}
		return population[0];
	}
	
	public double bestErrorValue(){
		return population[0].getFitness();
	}
	
	public double sumErrorVal(){
		double sum = 0.0;
		for(int i = 0; i < popSize; i++)sum += population[i].getFitness();
		return sum;
	}
	
	public Hashtable<Integer, Double> getBestError() {
		return bestError;
	}

	public Hashtable<Integer, Double> getSumError() {
		return sumError;
	}

	public Genome selectByRank(){
		int s = ((popSize)*(popSize-1))/2;
		double r = rand.nextDouble();
		int p = (int)(r*s);
		int n = (int)((1+Math.sqrt(1+(8*p)))/2);
		return population[popSize-n-1];
	}
	
	public Genome[] crossover(Genome p1, Genome p2){
		Genome[] children = new Genome[2];
		//For each of the groups of weights
		double[][][] child1 = new double[topology.length-1][][];
		double[][][] child2 = new double[topology.length-1][][];
		for(int i = 0; i < topology.length-1; i++){
			int cutPoint = rand.nextInt(topology[i]+1);
			child1[i] = new double[topology[i]+1][topology[i+1]];
			child2[i] = new double[topology[i]+1][topology[i+1]];
			for(int j = 0; j < cutPoint; j++){
				child1[i][j] = p1.getWeights()[i][j];
				child2[i][j] = p2.getWeights()[i][j];
			}
			for(int j = cutPoint; j < topology[i]+1; j++){
				child1[i][j] = p2.getWeights()[i][j];
				child2[i][j] = p1.getWeights()[i][j];
			}
		}
		children[0] = new Genome(child1);
		children[0].setFitness(fitness(children[0].getWeights()));
		children[0].mutate(mutateRate);
		children[1] = new Genome(child2);
		children[1].setFitness(fitness(children[1].getWeights()));
		children[1].mutate(mutateRate);
		return children;
	}
	
	public Genome[] crossover2(Genome p1, Genome p2){
		Genome[] children = new Genome[2];
		double[][][] child1 = new double[topology.length-1][][];
		double[][][] child2 = new double[topology.length-1][][];
		for(int i = 0; i < topology.length-1; i++){
			double[][] w1 = p1.getWeights()[i];
			double[][] w2 = p2.getWeights()[i];
			int crossover = rand.nextInt(w1.length*w1[0].length);
			double[][][] newW = ANNMatrix.crossover(w1, w2, crossover);
			child1[i] = newW[0];
			child2[i] = newW[1];
		}
		children[0] = new Genome(child1);
		children[0].setFitness(fitness(children[0].getWeights()));
		children[0].mutate(mutateRate);
		children[1] = new Genome(child2);
		children[1].setFitness(fitness(children[1].getWeights()));
		children[1].mutate(mutateRate);
		return children;
	}
	public double fitness(double[][][] weights){
		double fitness = 0.0;
		for(int j = 0; j < problem.trainingSet().length; j++){
			try{
				ann.setWeights(weights);
				double[][] outputs = ann.getOutputs(problem.trainingSet()[j]);
				double[][] error = ANNMatrix.subtract(outputs,problem.trainingTargetSet()[j]);
				fitness += ANN.sumOfSquaredError(error);
			}
			catch(Exception e){
				e.printStackTrace();
			}
		}
		return fitness;
	}

	public Genome best(){
		return population[0];
	}
	
	public class Genome implements Comparable<Genome>{
		
		private double fitness;
		private double[][][] weights;
		//Rows in the weights control the influence of a single input in the next
		//layer. They are columns when the W'I is computed for O
		public Genome(double[][][] weights) {
			this.weights = weights;
		}
		
		public double[][][] getWeights(){
			return weights;
		}
		
		public void setFitness(double fitness){
			this.fitness = fitness;
		}
		
		public double getFitness(){
			return fitness;
		}
		
		public int compareTo(Genome arg0) {
			if(this.fitness<arg0.fitness)return -1;
			if(this.fitness>arg0.fitness)return 1;
			return 0;
		}
		
		public void mutate(double mutateRate){
			for(int i = 0; i < weights.length; i++){
				for(int j = 0; j < weights[i].length; j++){
					for(int k = 0; k < weights[i][j].length; k++){
						//weights can randomly shift by up to +-0.5 
						if(mutateRate>rand.nextDouble()){
							mutateCounter++;
							weights[i][j][k] += (rand.nextDouble()/5.0)-0.1;
						}
					}
				}
			}
		}
		
		public String toString(){
			String s = "Genome toString(), fitness = " + fitness +"\n\n";
			for(int i = 0; i < weights.length; i++){
				String w = "";
				for(double[] d:weights[i])w += Arrays.toString(d) + "\n";
				s += w + "\n";
			}
			return s;
		}
	}
	
	public static void printHashtable(Hashtable<Integer, Double> table){
		System.out.println("Printing Hashtable\n");
		for(int i = 0; i < table.size(); i = i+100)System.out.println(table.get(i));
		System.out.println();
	}
}
